#ifdef __ANDROID__
#include "ARenderer.h"
#include <android/native_window_jni.h>
#include <android/looper.h>
#include "AApp.h"
#include "AUtil.h"
#include "../../MgrController.h"
#include "../../Project/SQ/Util.h"
#include "../../Project/SSQK/Util.h"
#include "../../core/CMath.h"
#include "../../test/TestCase.h"

P TestCase::s_debugModelPos;


ASurfaceTexture* ARenderer::s_surfaceTexture = nullptr;
EGLDisplay ARenderer::s_eglDisplay = EGL_NO_DISPLAY;
EGLSurface ARenderer::s_eglSurface = EGL_NO_SURFACE;
EGLContext ARenderer::s_eglContext = EGL_NO_CONTEXT;

static int s_frameCnt = 0;
static float s_frameRate = 0;// Hz

struct DebugStatus {
	bool printFPS = false;
	bool printRecvRenderTimeConsume = false;
	bool makeCrashEvery5s = false;
};
static const DebugStatus DebugInfo;

static long s_modelRecvTimeMs = -1;
static int s_modelRecvNO = -1;

static int RestartTimes = -1;
static bool s_isEGLDestroyed = true;
static long long s_lastRenderingTimeMs = -1;

/**@note in MyRenderThread*/
void ARenderer::createEGLContext(ASurfaceTexture* surfaceTexture) {
	auto ok = AApp::attachCurrentThread();
	ASSERT(ok, "Failed to attachCurrentThread!");

	ANativeWindow* nativeWindow = ASurfaceTexture_acquireANativeWindow(surfaceTexture);

	EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
	ASSERT(display != EGL_NO_DISPLAY, "Failed to get EGL display!");

	EGLint major, minor;
	EGLBoolean initSuccess = eglInitialize(display, &major, &minor);
	ASSERT(initSuccess, "Failed to initialize EGL!");

	std::vector<EGLint> configAttribs;
	configAttribs.insert(configAttribs.end(), {
			EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, // request OpenGL ES 3.x
			EGL_RED_SIZE, 8,        // red-channel: 8bit
			EGL_GREEN_SIZE, 8,      // green-channel: 8bit
			EGL_BLUE_SIZE, 8,       // blue-channel: 8bit
			EGL_ALPHA_SIZE, 8,      // alpha-channel: 8bit
			EGL_DEPTH_SIZE, 24,     // depth-buffer-channel: 24bit
	});
	int multiSamples = Config::Render_SurfaceSamples;
	if (multiSamples > 1) {
		configAttribs.insert(configAttribs.end(), {
				EGL_SAMPLE_BUFFERS, 1,		// enable multisampling
				EGL_SAMPLES, multiSamples,  // 4x multisampling
		});
	}
	configAttribs.push_back(EGL_NONE);// always terminate with EGL_NONE

	EGLConfig config;
	EGLint numConfigs;
	eglChooseConfig(display, configAttribs.data(), &config, 1, &numConfigs);
	ASSERT(numConfigs > 0, "Failed to choose EGL config!")

	EGLSurface eglSurface = eglCreateWindowSurface(display, config, nativeWindow, nullptr);
	ASSERT(eglSurface != EGL_NO_SURFACE, "Failed to create EGL Window Surface!")

	const EGLint attribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE };// request OpenGL ES 3.x version
	EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, attribs);
	ASSERT(context != EGL_NO_CONTEXT, "Failed to create EGL context!");

	EGLBoolean success = eglMakeCurrent(display, eglSurface, eglSurface, context);
	ASSERT(success, "Failed to make current for EGL surface!");

	s_surfaceTexture = surfaceTexture;
	s_eglDisplay = display;
	s_eglSurface = eglSurface;
	s_eglContext = context;

	int surfaceW = ANativeWindow_getWidth(nativeWindow);
	int surfaceH = ANativeWindow_getHeight(nativeWindow);
	Config::resize3DView(surfaceW, surfaceH);

	Scene::init(new AGLProcLoader);
	LogI << "Native Lib BuildTime: " << Config::BuildTime << ", cfgVer: " << Config::Version_Code;
	LogI << "GL Renderer and Scene is initialized successfully!       [restartTimes: " << ++RestartTimes << "]";
	s_isEGLDestroyed = false;
	AApp::registerSignalHandler();
	try {
		startRendering();
	} catch (const std::runtime_error& e) {
		LogE << "Caught SignalException: " << e.what() << "\n";
	}

	MgrController::releaseAll();
	destroyEGL();

//	AApp::detachCurrentThread();
	Scene::switchRenderingStatus(false);
	s_isEGLDestroyed = true;
}

/**@note in MyRenderThread*/
void ARenderer::startRendering() {
	TestCase::run();

	while (Scene::isRendering()) {
		++s_frameCnt;
		s_lastRenderingTimeMs = CUtil::currentTimeMillis();
		int modelRecvNO = s_modelRecvNO;

		MgrController::updateAllEntities();

		swapBuffer();

		CUtil::msleep(1);// proactively give up CPU time slices

		printDebugInfo(modelRecvNO);
	}
}

void ARenderer::destroyEGL() {
	if (s_eglDisplay != EGL_NO_DISPLAY) {
		eglMakeCurrent(s_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
		eglDestroySurface(s_eglDisplay, s_eglSurface);
		eglDestroyContext(s_eglDisplay, s_eglContext);
		eglTerminate(s_eglDisplay);
		s_eglDisplay = EGL_NO_DISPLAY;
		s_eglSurface = EGL_NO_SURFACE;
		s_eglContext = EGL_NO_CONTEXT;
		ASurfaceTexture_release(s_surfaceTexture);
	}
	LogI << "ARenderer:: EGL Surface is destroyed!!!      [Scene::isRendering() = " << std::boolalpha << Scene::isRendering() << "]";
}

void ARenderer::printDebugInfo(int modelRecvNO) {
	if (s_frameCnt % 600 == 0) {// log every 10s on average
		static CElapsedTimer t;
		s_frameRate = 600.0f * 1000 / t.elapsed();
		LogI << "frame cnt = " << s_frameCnt << std::fixed << std::setprecision(1) << " [FPS:" << s_frameRate << "]";
		t.reset();
	}
	
	if (Config::Released)
		return;
		
	if (DebugInfo.makeCrashEvery5s) {
		if (s_frameCnt % 300 == 0) {// crash every 5s
			LogE << "crashing xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (manually)";
			int *i;
			*i = 100;
		}
	}
	if (DebugInfo.printRecvRenderTimeConsume) {
		if (modelRecvNO == s_modelRecvNO)
			LogI << "NO: #" << s_modelRecvNO << ", Time consumes(from Recv to Render-Done): " << (int) (CUtil::currentTimeMillis() - s_modelRecvTimeMs) << "ms";
	}

	if (DebugInfo.printFPS) {
		static CElapsedTimer timer;
		static int GapFrameN = 10;
		static int total = 0;
		static int i = 0;
		auto elapsed = timer.elapsed();
		timer.reset();
		total += elapsed;
		if (++i == GapFrameN) {
			LogI << "************************ FPS(ms): " << std::fixed << std::setprecision(1) << (float(total) / GapFrameN);
			i = 0;
			total = 0;
		}
	}
}


/**@note in MyRenderThread(created from Java Thread), create egl context in the new thread as render-thread*/
extern "C" JNIEXPORT void JNICALL
Java_com_hb_sq3d_Viewer3D_createEGLContext(JNIEnv* env, jclass /* jclz */, jobject surfaceTexture) {
	//global_surfaceTexture = env->NewGlobalRef(surfaceTexture);// make it accessible in different threads
	auto surfaceTex = ASurfaceTexture_fromSurfaceTexture(env, surfaceTexture);// note: has arg 'env', and every time coming a new surfaceTexture
	ARenderer::createEGLContext(surfaceTex);
}

extern "C" JNIEXPORT void JNICALL
Java_com_hb_sq3d_Viewer3D_destroyEGLContext(JNIEnv* env, jclass /* jclz */) {
	Scene::switchRenderingStatus(false);
	LogI << "Java_com_hb_sq3d_Viewer3D_destroyEGLContext been called! Scene::isRendering()=" << std::boolalpha << Scene::isRendering();
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_hb_sq3d_Viewer3D_isEGLDestroyed(JNIEnv* env, jclass /* jclz */) {
	if (s_frameCnt < 10)
		return false;
	return s_isEGLDestroyed;
}

extern "C" JNIEXPORT jboolean JNICALL
Java_com_hb_sq3d_Viewer3D_isRendering(JNIEnv* env, jclass /* jclz */) {
	// loading zhiche and many entities may consume more than 3s
	if (s_frameCnt < 10)
		return true;
	return CUtil::currentTimeMillis() - s_lastRenderingTimeMs < 5000;
}

extern "C" JNIEXPORT jfloat JNICALL
Java_com_hb_sq3d_Viewer3D_frameRate(JNIEnv* env, jclass /* jclz */) {
	return s_frameRate;
}

/**@note in MainThread*/
extern "C" JNIEXPORT void JNICALL
Java_com_hb_sq3d_Viewer3D_setReceivingDataEnabled(JNIEnv* env, jclass /* jclz */, jboolean enabled) {
	Cache::setReceivingData(enabled);
	LogI << "Viewer3D is " << (enabled ? "shown" : "hidden") << ", enable receiving data: " << std::boolalpha << (bool)enabled;
}

/**@note in MainThread*/
extern "C" JNIEXPORT void JNICALL
Java_com_hb_sq3d_Viewer3D_onViewportChanged(JNIEnv *env, jclass /* jclz */, jint width, jint height) {
	Config::resize3DView(width, height);
	LogI << "onViewportChanged => width x height = " << width << " x " << height;
}

/**@note in MainThread*/
extern "C" JNIEXPORT jint JNICALL
Java_com_hb_sq3d_Viewer3D_frameUpdate(JNIEnv *env, jclass /* jclz */) {
	const int FRAME_UPDATE_STATUS_ZHICHE_RENDERED_DONE = 0x100;// keep same as Viewer3D.java
	return (ZhicheEntity::hasRenderedDone() ? 1 : -1) * FRAME_UPDATE_STATUS_ZHICHE_RENDERED_DONE;
}

extern "C" JNIEXPORT void JNICALL
Java_com_hb_sq3d_Viewer3D_recvModels(JNIEnv *env, jclass clazz, jobject allModelArgs, jlong timeMs, jint number) {
	if (!Scene::isRendering()) return;
	s_modelRecvTimeMs = timeMs;
	s_modelRecvNO = number;
	auto idsPair = Get_IntArray_Ptr(allModelArgs, "ids");
	auto typesPair = Get_IntArray_Ptr(allModelArgs, "types");
	auto hDistsPair = Get_FloatArray_Ptr(allModelArgs, "hDists");
	auto vDistsPair = Get_FloatArray_Ptr( allModelArgs, "vDists");
	auto anglesPair = Get_FloatArray_Ptr( allModelArgs, "angles");

	std::pair<jint*, jintArray> recogsPair = std::make_pair(nullptr, nullptr);
	if (Proj::isSSQK())
		recogsPair = Get_IntArray_Ptr( allModelArgs, "recogs");

	auto idsPtr = idsPair.first;
	auto typesPtr = typesPair.first;
	auto hDistsPtr = hDistsPair.first;
	auto vDistsPtr = vDistsPair.first;
	auto anglesPtr = anglesPair.first;
	int* recogsPtr = nullptr;
	if (Proj::isSSQK())
		recogsPtr = recogsPair.first;

	jsize N = env->GetArrayLength(idsPair.second);
	CVector<sq::ModelArg> modelArgs;
	for (int i = 0; i < N; i++) {
		sq::ModelArg model;
		model.id = idsPtr[i];
		if (Proj::isSQ()) {
			model.type = sq::Util::modelType(typesPtr[i]);
			model.pos = sq::Util::modelPos(hDistsPtr[i], vDistsPtr[i]);
		} else if (Proj::isSSQK()) {
			model.type = ssqk::Util::modelType(typesPtr[i]);
			model.pos = ssqk::Util::modelPos(hDistsPtr[i], vDistsPtr[i]);
		}
		model.angle = CMath::radians(anglesPtr[i]);
		if (Proj::isSSQK()) {
			model.angle = 0;// force 'angle' Zero
			model.recog = recogsPtr[i];
		}
		modelArgs << model;
	}

	RELEASE_ARRAY(idsPair, typesPair, hDistsPair, vDistsPair, anglesPair);
	if (Proj::isSSQK())
		RELEASE_ARRAY(recogsPair);

	CVector<ModelArg> ktModels;
	if (Proj::isSQ())
		sq::Util::toKuantiData(ktModels, modelArgs);
	else if (Proj::isSSQK())
		ssqk::Util::toKuantiData(ktModels, modelArgs);
	Cache::recvModels(ktModels);
}

extern "C" JNIEXPORT void JNICALL
Java_com_hb_sq3d_Viewer3D_recvLines(JNIEnv *env, jclass /* jclz */, jobject allLineArgs) {
	if (!Scene::isRendering()) return;
	auto idsPair = Get_IntArray_Ptr(allLineArgs, "ids");
	auto stylesPair = Get_IntArray_Ptr(allLineArgs, "styles");
	auto coloursPair = Get_IntArray_Ptr(allLineArgs, "colours");
	auto isPointsDatasPair = Get_BoolArray_Ptr(allLineArgs, "isPointsDatas");

	auto eqCsPair = Get_Float2DArray_Ptr(allLineArgs, "Cs");
	auto eqStartsPair = Get_FloatArray_Ptr(allLineArgs, "starts");
	auto eqEndsPair = Get_FloatArray_Ptr(allLineArgs, "ends");

	auto linePointsValuesNumsPair = Get_IntArray_Ptr(allLineArgs, "linePointsValuesNums");
	auto allPointsValuesPair = Get_Float2DArray_Ptr(allLineArgs, "allPointsValues");

	auto idsPtr = idsPair.first;
	auto stylesPtr = stylesPair.first;
	auto coloursPtr = coloursPair.first;

	auto isPointsDatasPtr = isPointsDatasPair.first;
	auto eqCsPtr = eqCsPair.first;
	auto eqStartsPtr = eqStartsPair.first;
	auto eqEndsPtr = eqEndsPair.first;
	auto linePointsValuesNumsPtr = linePointsValuesNumsPair.first;
	auto allPointsValuesPtr = allPointsValuesPair.first;

	jsize N = env->GetArrayLength(idsPair.second);
	CVector<sq::LineArg> lineArgs;
	for (int i = 0; i < N; i++) {
		bool isPointsData = isPointsDatasPtr[i];
		sq::LineArg line;
		if (Proj::isSQ()) {
			line.style = sq::Util::lineStyle(stylesPtr[i]);
			line.color = sq::Util::lineColor(coloursPtr[i]);
		} else if (Proj::isSSQK()) {
			line.style = ssqk::Util::lineStyle(stylesPtr[i]);
			line.color = ssqk::Util::lineColor(coloursPtr[i]);
		}
		if (!isPointsData) {
			line.eq.C = CVector<float>(eqCsPtr[i], eqCsPtr[i] + 4);
			line.eq.zStart = -eqStartsPtr[i];
			line.eq.zEnd = -eqEndsPtr[i];
		} else {
			int pointsValuesNum = linePointsValuesNumsPtr[i];
			for (int k = 0; k < pointsValuesNum; k += 2) {
				P p;
				p.x = -allPointsValuesPtr[i][k];
				p.z = -allPointsValuesPtr[i][k+1];
				line.eq.points << p;
			}
		}
		lineArgs << line;
	}

	RELEASE_ARRAY(idsPair, stylesPair, coloursPair, isPointsDatasPair, eqStartsPair, eqEndsPair, linePointsValuesNumsPair);
	RELEASE_2DARRAY(eqCsPair, allPointsValuesPair);

	CVector<LineArg> ktLineArgs;
	if (Proj::isSQ())
		sq::Util::toKuantiData(ktLineArgs, lineArgs);
	else if (Proj::isSSQK())
		ssqk::Util::toKuantiData(ktLineArgs, lineArgs);
	Cache::recvLines(ktLineArgs);
}

extern "C" JNIEXPORT void JNICALL
Java_com_hb_sq3d_Viewer3D_recvStatus(JNIEnv *env, jclass /* jclz */, jobject status) {
	if (!Scene::isRendering()) return;
	sq::Status sqStatus;
	sqStatus.acc = sq::Util::acc(Get_IntValue(status, "acc"));
	sqStatus.fcw = sq::Util::fcw(Get_IntValue(status, "fcw"));
	sqStatus.alc = sq::Util::alc(Get_IntValue(status, "alc"));
	int crossedLine = Get_IntValue(status, "crossed");
	if (crossedLine == 1)
		sqStatus.alc = sq::ALC::Off;
	sqStatus.noa = sq::Util::noa(Get_IntValue(status, "noa"));
	sqStatus.acas = sq::Util::acas(Get_IntValue(status, "acas"));
	Cache::updateZhicheSpeed(Get_IntValue(status, "zhicheSpeed"));
	//Cache::updateZhicheAngle(CMath::radians(Get_FloatValue(status, "zhicheAngle")));
	Cache::updateSQStatus(sqStatus);
}

extern "C" JNIEXPORT void JNICALL
Java_com_hb_sq3d_Viewer3D_changeDarkMode(JNIEnv *env, jclass /* jclz */, jboolean isDarkMode) {
	if (!Scene::isRendering()) return;
	LogI << "isDarkMode: " << std::boolalpha << (bool)isDarkMode;
	Global::setDarkMode(isDarkMode);
}

#endif